package com.definity.toolkit.persistence;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.apache.commons.beanutils.PropertyUtils;

import com.definity.toolkit.domain.Domain;
import com.definity.toolkit.service.ServiceException;


public class RepositoryJPA<T extends Domain<? extends Serializable>> implements Repository<T> {
	
	protected final Class<T> domainType;
	protected final EntityManager entityManager;
	private List<RepositoryListener> listeners;

	public RepositoryJPA(EntityManager entityManager, Class<T> domain) {
		this.domainType = domain;
		this.entityManager = entityManager;
	}
	
	public T save(T domain) {
		try {
			T merged = entityManager.merge(domain);
			entityManager.persist(merged);
			
			return merged;
		} catch (Exception e) {
			throw new DatabaseException("save", e);
		}
	}
	
	public void delete(T domain) {
		T foundDomain = findById(domain.getId());
		if (foundDomain == null)
			throw new DatabaseException("notexists");
		
		try {
			entityManager.remove(foundDomain);
		} catch (Exception e) {
			throw new DatabaseException("delete", e);
		}
	}
	
	public T findById(Serializable id) {
		if (id == null)
			return null;
		
		T persistedDomain;
		try {
			persistedDomain = entityManager.find(domainType, id);
		} catch (Exception e) {
			return null;
		}
		return persistedDomain;
	}

	public List<T> findBy(T example, String... attributes) {
		CriteriaBuilder criteria = entityManager.getCriteriaBuilder();
		CriteriaQuery<T> query = criteria.createQuery(domainType);
		Root<T> from = query.from(domainType);
		query.select(from);
		
		Predicate[] clauses = new Predicate[attributes.length];
		
		int i = 0;
		for (String attribute : attributes) {
			Object value = getAttributeValue(example, attribute);
			clauses[i++] = criteria.equal(from.get(attribute), value); 
		}

		query.where(clauses);
		
		return entityManager.createQuery(query).getResultList();
	}
	
	public <I extends Serializable> List<T> memberOf(String attribute, Domain<I> domain) {
		CriteriaBuilder criteria = entityManager.getCriteriaBuilder();
		CriteriaQuery<T> query = criteria.createQuery(domainType);
		Root<T> from = query.from(domainType);
		query.select(from);
		query.where(criteria.isMember(domain, from.<List<Domain<I>>>get(attribute)));
		
		return entityManager.createQuery(query).getResultList();
	}
	
	private Object getAttributeValue(T example, String attribute) {
		try {
			return PropertyUtils.getProperty(example, attribute);
		} catch (IllegalAccessException e) {
			throw new ServiceException(e.getMessage(), e);
		} catch (InvocationTargetException e) {
			throw new ServiceException(e.getMessage(), e);
		} catch (NoSuchMethodException e) {
			throw new ServiceException(e.getMessage(), e);
		}
	}

	@SuppressWarnings("unchecked")
	public List<T> find(String hql, Object... parameters) {
		Query query = createQuery(hql);
		for (int i = 0; i < parameters.length; i++) {
			query.setParameter(i + 1, parameters[i]);
		}
		return query.getResultList();
	}
	
	public Query createQuery(String hql) {
		return entityManager.createQuery(hql);
	}

	public Query createNativeQuery(String hql) {
		return entityManager.createNativeQuery(hql);
	}

	public Query createNativeQuery(String hql, Class<?> type) {
		return entityManager.createNativeQuery(hql, type);
	}
	
	public CriteriaBuilder createCriteriaBuilder() {
		return entityManager.getCriteriaBuilder();
	}
	
	public List<T> findBy(CriteriaQuery<T> criteria) {
		return entityManager.createQuery(criteria).getResultList();
	}
	
	public List<T> listAll() {
		CriteriaBuilder builder = entityManager.getCriteriaBuilder();
		CriteriaQuery<T> query = builder.createQuery(domainType);
		query.select(query.from(domainType));
		return entityManager.createQuery(query).getResultList();
	}

	@Override
	public void flush() {
		entityManager.flush();
	}
	
	@Override
	public void addRepositoryListener(RepositoryListener listener) {
		listeners.add(listener);
	}
	
	@Override
	public void removeRepositoryListener(RepositoryListener listener) {
		listeners.remove(listener);
	}

	@Override
	public List<RepositoryListener> getListeners() {
		return listeners;
	}

}
